package dd.lo.video;

import dd.lo.HelloCV;
import dd.lo.util.ImageUtils;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.commons.lang3.StringUtils;
import org.opencv.core.Mat;
import org.opencv.core.MatOfByte;
import org.opencv.core.Size;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;
import org.opencv.videoio.VideoCapture;
import org.opencv.videoio.Videoio;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.ImageWriteParam;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataNode;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;

public class VideoToGifUtil extends Application {

    public static void main(String[] args) {
        launch(args);
    }

    private static final String OUTPUT_DIR = "/Users/kwok/Downloads/";

    private static final int DEFAULT_WIDTH = 1080;
    private static final int DEFAULT_HEIGHT = 768;

    private VideoCapture cap;

    private File videoFile;

    private AtomicBoolean playStatus;

    private AtomicBoolean saveStatus;

    private ImageView frameView;

    private Text progressTxt;

    private int totalFrame;

    private int playFrame;

    private int fps;

    private int vw;
    private int vh;

    private Slider slider;

    private void play() {
        boolean acquired = playStatus.compareAndSet(false, true);
        if (!acquired) {
            playStatus.set(false);
            return;
        }
        boolean saving = saveStatus.get();
        if (saving) {
            showAlert("任务导出中，请勿执行其他操作", "播放失败");
            return;
        }
        //下面将更新视频图像的工作交由一个异步线程执行，如果在UI线程（主线程）中执行图像更新操作，则会一直卡死程序
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                cap.set(Videoio.CAP_PROP_POS_FRAMES, playFrame);
                Mat frame = new Mat();
                //buffer用于保存OpenCV读取出来的视频帧的编码后的图像信息
                MatOfByte buffer = new MatOfByte();
                while (true) {
                    if (!playStatus.get()) {
                        //非播放状态，退出循环
                        break;
                    }
                    if (playFrame >= totalFrame) {
                        //到达视频结尾，暂停并结束视频播放任务
                        playFrame = 0;
                        break;
                    }
                    cap.read(frame);
                    ++playFrame;
                    if (frame.empty()) {
                        //如果读不出图像，或播放状态为false，就可以认为是到了视频结尾，结束视频播放任务
                        showAlert("播放失败，部分帧不能解析", "温馨提示");
                        break;
                    }
                    ImageUtils.fitImgSize(frame, DEFAULT_WIDTH, DEFAULT_HEIGHT);
                    //读取到帧数据，将帧的Mat转换为MatOfBuffer
                    Imgcodecs.imencode(".png", frame, buffer);
                    Platform.runLater(() -> {
                        //需要修改ImageView，因此也放到Platform.runLater中更新，此时可以用ByteArrayInputStream转换MatOfByte来显示到ImageView中
                        frameView.setImage(new Image(new ByteArrayInputStream(buffer.toArray())));
                        slider.setValue(playFrame);
                    });
                }
                if (playFrame == 0) {
                    showAlert("播放完成", "温馨提示");
                }
                playStatus.set(false);
                return null;
            }
        };
        new Thread(task).start();
    }

    private Node findNode(Node root, String nodeName) {
        if (StringUtils.equals(nodeName, root.getNodeName())) {
            return root;
        }
        NodeList childNodes = root.getChildNodes();
        for (int i = 0; i < childNodes.getLength(); ++i) {
            Node target = findNode(childNodes.item(i), nodeName);
            if (null != target) return target;
        }
        return null;
    }

    private void saveToGif() {
        boolean acquired = saveStatus.compareAndSet(false, true);
        if (!acquired) {
            showAlert("任务导出中，请勿重复操作", "导出失败");
            return;
        }
        //停止播放，从头开始导出
        playStatus.set(false);
        playFrame = 0;
        //下面将更新视频图像的工作交由一个异步线程执行，如果在UI线程（主线程）中执行图像更新操作，则会一直卡死程序
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() {
                String videoFileName = videoFile.getName();
                cap.set(Videoio.CAP_PROP_POS_FRAMES, 0);
                Mat frame0 = new Mat();
                cap.read(frame0);
                MatOfByte buffer0 = new MatOfByte();
                Imgcodecs.imencode(".jpg", frame0, buffer0);
                cap.set(Videoio.CAP_PROP_POS_FRAMES, 0);
                Mat frame = new Mat();
                //buffer用于保存OpenCV读取出来的视频帧的编码后的图像信息
                MatOfByte buffer = new MatOfByte();
                //准备生成GIF的文件
                Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("GIF");
                if (!writers.hasNext()) {
                    playFrame = 0;
                    Platform.runLater(() -> progressTxt.setText(""));
                    saveStatus.set(false);
                    showAlert("系统不支持GIF生成", "导出失败");
                    return null;
                }
                ImageWriter gifWriter = writers.next();
                ImageWriteParam gifWriteParam = new ImageWriteParam(Locale.CHINESE);
                IIOMetadata metadata = null;
                try {
                    if (gifWriteParam.canWriteCompressed()) {
                        gifWriteParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
                    }
                    gifWriter.setOutput(ImageIO.createImageOutputStream(new File(OUTPUT_DIR + videoFileName.substring(0, videoFileName.lastIndexOf(".")) + ".gif")));
                    //准备元数据
                    metadata = gifWriter.getDefaultImageMetadata(new ImageTypeSpecifier(ImageIO.read(new ByteArrayInputStream(buffer0.toArray()))), gifWriteParam);
                    Node root = metadata.getAsTree(metadata.getNativeMetadataFormatName());
                    IIOMetadataNode graphicControlExtension = (IIOMetadataNode) findNode(root, "GraphicControlExtension");
                    if (null != graphicControlExtension) {
                        System.out.printf("delay: %s\n", graphicControlExtension.getAttribute("delayTime"));
                        graphicControlExtension.setAttribute("delayTime", "1");
                    }
                    //准备写入
                    gifWriter.prepareWriteSequence(metadata);
                } catch (Exception e) {
                    playFrame = 0;
                    Platform.runLater(() -> progressTxt.setText(""));
                    saveStatus.set(false);
                    showAlert("创建GIF图片失败", "导出失败");
                    return null;
                }
                while (true) {
                    if (playFrame >= totalFrame) {
                        //到达视频结尾，暂停并结束视频播放任务
                        playFrame = 0;
                        playStatus.set(false);
                        saveStatus.set(false);
                        break;
                    }
                    cap.read(frame);
                    ++playFrame;
                    if (frame.empty()) {
                        //如果读不出图像，或播放状态为false，就可以认为是到了视频结尾，暂停并结束视频播放任务
                        playStatus.set(false);
                        saveStatus.set(false);
                        break;
                    }
                    Imgproc.resize(frame, frame, new Size(frame.width() / 4.0, frame.height() / 4.0));
                    //读取到帧数据，将帧的Mat转换为MatOfBuffer
                    Imgcodecs.imencode(".jpg", frame, buffer);
                    //每3帧写入一帧
                    if (playFrame % 3 != 0) continue;
                    try {
                        gifWriter.writeToSequence(new IIOImage(ImageIO.read(new ByteArrayInputStream(buffer.toArray())), null, metadata), gifWriteParam);
                    } catch (Exception e) {
                        playFrame = 0;
                        Platform.runLater(() -> progressTxt.setText(""));
                        saveStatus.set(false);
                        showAlert("写入GIF图片失败", "导出失败");
                        return null;
                    }
                    Platform.runLater(() -> progressTxt.setText(String.format("导出中，处理进度%d/%d（帧数）", playFrame, totalFrame)));
                }
                try {
                    gifWriter.endWriteSequence();
                } catch (IOException e) {
                    playFrame = 0;
                    Platform.runLater(() -> progressTxt.setText(""));
                    saveStatus.set(false);
                    showAlert("结束写入GIF图片失败", "导出失败");
                    return null;
                }
                gifWriter.dispose();
                showAlert("任务导出完成");
                playFrame = 0;
                Platform.runLater(() -> progressTxt.setText(""));
                saveStatus.set(false);
                return null;
            }
        };
        new Thread(task).start();
    }

    private void showAlert(String msg, String header) {
        Platform.runLater(() -> {
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("信息提示");
            alert.setHeaderText(header);
            alert.setContentText(msg);
            alert.showAndWait();
        });
    }

    private void showAlert(String msg) {
        showAlert(msg, "请注意");
    }

    @Override
    public void start(Stage stage) {
        String path = HelloCV.class.getResource("/libopencv_java481.dylib").getPath();
        System.out.println(path);
        System.load(path);
        playStatus = new AtomicBoolean(false);
        saveStatus = new AtomicBoolean(false);
        //Image view for frame
        frameView = new ImageView();
        //Progress text
        progressTxt = new Text();
        //File chooser
        Button videoChooser = new Button("选择视频文件");
        videoChooser.setOnMouseClicked(e -> {
            FileChooser fileChooser = new FileChooser();
            fileChooser.setTitle("请选择视频文件");
            fileChooser.setInitialDirectory(new File(System.getProperty("user.home") + "/Downloads")
            );
            fileChooser.getExtensionFilters().addAll(
                    new FileChooser.ExtensionFilter("MP4", "*.mp4")
            );
            videoFile = fileChooser.showOpenDialog(stage);
            if (null == videoFile) {
                showAlert("您没有选中文件，请选择文件后再操作");
                return;
            }
            try {
                cap = new VideoCapture(videoFile.getAbsolutePath());
            } catch (Exception ex) {
                showAlert("您选择文件可能不是视频文件，读取失败");
                return;
            }
            totalFrame = (int) cap.get(Videoio.CAP_PROP_FRAME_COUNT);
            slider.setMax(totalFrame);
            fps = (int) cap.get(Videoio.CAP_PROP_FPS);
            vw = (int) cap.get(Videoio.CAP_PROP_FRAME_WIDTH) / 2;
            vh = (int) cap.get(Videoio.CAP_PROP_FRAME_HEIGHT) / 2;
            play();
        });
        //Save button
        Button saveBtn = new Button("保存为GIF");
        saveBtn.setOnMouseClicked(event -> saveToGif());
        //Play button
        Button playBtn = new Button("播放/暂停");
        playBtn.setOnMouseClicked(event -> play());
        //Scroll bar
        slider = new Slider(0, 1000, 0);
        slider.setPrefWidth(DEFAULT_WIDTH * 0.77);
        //Ensemble all nodes
        HBox fileControl = new HBox();
        fileControl.setPadding(new Insets(5, 5, 5, 5));
        fileControl.getChildren().addAll(videoChooser, saveBtn, progressTxt);
        HBox videoControl = new HBox();
        videoControl.setPadding(new Insets(5, 5, 5, 5));
        videoControl.getChildren().addAll(playBtn, slider);
        VBox vBox = new VBox();
        vBox.setPadding(new Insets(0, 0, 0, 0));
        vBox.getChildren().addAll(fileControl, videoControl);
        BorderPane root = new BorderPane();
        root.setCenter(frameView);
        root.setBottom(vBox);
        Scene scene = new Scene(root, DEFAULT_WIDTH, DEFAULT_HEIGHT + 80);
        stage.setScene(scene);
        stage.setTitle("Video2GIF");
        stage.setResizable(false);
        stage.show();
    }
}
